package com.example.plantuml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

/**
 * Launches a local PlantUML HTTP server from a bundled plantuml.jar using picoweb.
 * Starts at base port and increments by 1 if occupied.
 */
public final class LocalPlantUmlServerLauncher {
    private static volatile Process serverProcess;
    private static volatile int listeningPort = -1;

    private LocalPlantUmlServerLauncher() {}

    /**
     * Attempts to start a local server at the first free port starting from basePort.
     * Returns the base URL (e.g., http://127.0.0.1:9996) if started successfully; otherwise null.
     */
    public static String startIfPossible(int basePort) {
        if (serverProcess != null && serverProcess.isAlive() && listeningPort > 0) {
            return "http://127.0.0.1:" + listeningPort;
        }

        File jar = resolvePlantUmlJar();
        if (jar == null || !jar.isFile()) {
            return null;
        }

        int port = findFirstFreePort(basePort, basePort + 50);
        if (port <= 0) {
            return null;
        }

        List<String> cmd = new ArrayList<>();
        cmd.add(resolveJavaBinary());
        cmd.add("-jar");
        cmd.add(jar.getAbsolutePath());
        cmd.add("-picoweb:" + port);

        ProcessBuilder pb = new ProcessBuilder(cmd);
        pb.directory(jar.getParentFile());
        pb.redirectErrorStream(true);
        try {
            serverProcess = pb.start();
            drainOutputAsync(serverProcess);
        } catch (IOException e) {
            return null;
        }

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try { if (serverProcess != null) serverProcess.destroy(); } catch (Throwable ignored) {}
        }));

        // Wait up to ~8 seconds for the server to respond
        long deadline = System.currentTimeMillis() + 8000L;
        String baseUrl = "http://127.0.0.1:" + port;
        while (System.currentTimeMillis() < deadline) {
            if (isHttpResponsive(baseUrl)) {
                listeningPort = port;
                return baseUrl;
            }
            try { Thread.sleep(200); } catch (InterruptedException ignored) {}
        }

        // Failed to start
        try { serverProcess.destroyForcibly(); } catch (Throwable ignored) {}
        serverProcess = null;
        listeningPort = -1;
        return null;
    }

    private static File resolvePlantUmlJar() {
        // Try to find any plantuml*.jar in likely locations, prefer the newest file
        File[] roots = new File[] {
                new File(System.getProperty("user.dir", ".")),
                new File(System.getProperty("user.dir", "."), "lib"),
                new File(System.getProperty("user.dir", "."), "deps")
        };
        File best = null;
        for (File dir : roots) {
            if (dir != null && dir.isDirectory()) {
                File[] files = dir.listFiles((d, name) -> name.toLowerCase().startsWith("plantuml") && name.toLowerCase().endsWith(".jar"));
                if (files != null) {
                    for (File f : files) {
                        if (best == null || f.lastModified() > best.lastModified()) {
                            best = f;
                        }
                    }
                }
            }
        }
        return best;
    }

    private static String resolveJavaBinary() {
        String javaHome = System.getProperty("java.home");
        File bin = new File(javaHome, "bin");
        if (isWindows()) {
            File javaw = new File(bin, "javaw.exe");
            if (javaw.isFile()) return javaw.getAbsolutePath();
        }
        File javaExe = new File(bin, isWindows() ? "java.exe" : "java");
        if (javaExe.isFile()) return javaExe.getAbsolutePath();
        return isWindows() ? "javaw" : "java"; // fallback to PATH
    }

    private static boolean isWindows() {
        String os = System.getProperty("os.name", "").toLowerCase();
        return os.contains("win");
    }

    private static int findFirstFreePort(int start, int endInclusive) {
        for (int p = start; p <= endInclusive; p++) {
            if (!isPortInUse("127.0.0.1", p)) return p;
        }
        return -1;
    }

    private static boolean isPortInUse(String host, int port) {
        try (Socket s = new Socket()) {
            s.connect(new InetSocketAddress(host, port), 200);
            return true; // connected => in use
        } catch (IOException ignored) {
            return false; // cannot connect => likely free
        }
    }

    private static boolean isHttpResponsive(String baseUrl) {
        try {
            HttpURLConnection conn = (HttpURLConnection) new URL(baseUrl + "/").openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(500);
            conn.setReadTimeout(1000);
            int code = conn.getResponseCode();
            return code >= 200 && code < 500; // any response means it's up
        } catch (IOException ignored) {
            return false;
        }
    }

    /** Stops the local server process if running. */
    public static void stopIfRunning() {
        try {
            if (serverProcess != null) {
                serverProcess.destroy();
                serverProcess = null;
            }
        } catch (Throwable ignored) {}
        listeningPort = -1;
    }

    private static void drainOutputAsync(Process p) {
        try {
            final InputStream in = p.getInputStream();
            Thread t = new Thread(() -> {
                byte[] buf = new byte[1024];
                try {
                    while (in.read(buf) != -1) { /* discard */ }
                } catch (IOException ignored) {
                } finally {
                    try { in.close(); } catch (IOException ignored2) {}
                }
            }, "plantuml-server-drain");
            t.setDaemon(true);
            t.start();
        } catch (Throwable ignored) {}
    }
}


